/*  
 *****************************************************************
 * {__} **********************************************************   
 *  }{  ********************************************************** 
 *****************************************************************
 *
 * JSPred Project
 * (c) Software Languages Lab, Vrije Universiteit Brussel, 2012
 * Author: Jorge Vallejos
 * 
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 *****************************************************************
 */

/* A PREDICATE DISPATCHER
 * 
 * In this implementation:
 * - A predicated function contains a number of variants, known as predicated methods.
 * - Order of definition of predicated methods determine their order of application.
 * - Predicates are defined as functions with the same parameter list as that of the    
 * predicated methods.
 * 
 */

// Extension to Function.prototype to support predicates
PredicatedMethod = function(predicate,body) {
  this.predicate = predicate;
  this.body = body;
}

// Main function to create a predicate function
function makePredicatedFunction(owner) {
  var methods = [];
  // Select applicable methods
  var computePredicatedFunction = function(args) {
    var applicableMethods = []
    for (var i = 0; i < methods.length; i++) {
      var method = methods[i];
      var applicable = method.predicate.apply(owner,args);
      if (applicable === true) {
        applicableMethods = applicableMethods.concat([method]);
      }
    }
    if (applicableMethods === []) {
      throw "There are no applicable methods.";
    }
    return applicableMethods;
  }
  // Compute function "next".
  // It returns a function which recursively calls the next applicable method.
  var computeNextFunction = function(applicableMethods,currentIndex,originalArguments) {
    var next = function() {
      throw "There are no more applicable methods.";
    };
    if (currentIndex < (applicableMethods.length - 1)) {
      currentIndex++;
      nextNext = computeNextFunction(applicableMethods,currentIndex,originalArguments);
      next = function() {
        var augmentedArguments = originalArguments.concat([nextNext]);
        return applicableMethods[currentIndex].body.apply(owner,augmentedArguments);
      }
    }
    return next;
  }
  // Get index of predicated method specialised on predicate given as argument 
  var findIndexOfMethodWithPredicate = function(predicate) {
    var index = -1;
    for(var i = 0; i < methods.length; i++) {
      if (predicate === methods[i].predicate) {
        index = i;
        break;
      }
    }
    return index;
  }
  // Predicated function
  // - compute applicable methods
  // - compute function next
  // - apply most specific applicable method 
  var dispatcher = function() {
    var receivedArguments = Array.prototype.slice.call(arguments, 0)
    var applicableMethods = computePredicatedFunction(receivedArguments);
    var next = computeNextFunction(applicableMethods,0,receivedArguments);
    var augmentedArguments = receivedArguments.concat([next]);
    return applicableMethods[0].body.apply(owner,augmentedArguments);
  }
  // Add a predicated method
  dispatcher.when = function(predicate,method){
    var predicatedMethod = new PredicatedMethod(predicate,method);
    var methodIndex = findIndexOfMethodWithPredicate(predicate);
    if (methodIndex < 0) {
      methods = methods.concat([predicatedMethod]);
    } else {
      methods[methodIndex] = predicatedMethod;
    }
  }
  return dispatcher;
} 

console.log("--> JSPred loaded successfully.");

/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
// TESTS
/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////

// CALCULATE FACTORIAL

// Define math object
var math = { };

// Define factorial predicate function
math.factorial = makePredicatedFunction(math);

math.factorial.when(
  function(n) { 
    return (n > 0);
  },
  function(n) {
    return (n * this.factorial(n-1));
  });

math.factorial.when(
  function(n) {
    return n === 0;
  },
  function(n) {
    return 1;
  });

math.factorial.when(
  function(n) {
    return (n < 0);
  },
  function(n) {
    throw "Factorial function does not support negative numbers";
  });

// Call factorial predicate function
console.log("Factorial of 5 is " + math.factorial(5));
console.log("Factorial of 10 is " + math.factorial(10));

// DISPLAY NUMBER PROPERTIES

// Define whatIs predicated function
math.whatIs = makePredicatedFunction();

math.whatIs.when(ever,
  function(n,next) {
    return console.log(n + " is" + next());
  });

math.whatIs.when(isPrime,
  function(n,next) {
    return " prime and" + next();
  });

math.whatIs.when(isOdd,
  function(n) {
    return " odd";
  });

math.whatIs.when(isEven,
  function(n) {
  return " even";       
  });

// replacing function specialised on isOdd predicate
math.whatIs.when(isOdd,
  function(n) {
    return " ODD";
  });

// Define predicates
function ever() {
  return true;
}

function isOdd(n) {
  return (n % 2) === 1;
}

function isEven(n) {
  return !(isOdd(n));
}

function isPrime(n) {
  var result = true;
  for (i = 2; (i*i) <= n; i++) {
    if ((n % i) === 0) {
      result = false;
      break;
    }
  }
  return result;
}


// Invoke whatIs predicated function
math.whatIs(2); // displays "2 is prime and even"
math.whatIs(3); // displays "3 is prime and odd"
math.whatIs(4); // displays "4 is even"
